//
// Created by haoy on 2017/9/15.
//

#include "codecs.hpp"

#include <algorithm>
#include <cctype>
#include <cstdlib>
#include <iomanip>
#include <iterator>
#include <string.h>

#include "xm_log.h"

/*
 * PRIVATE
 */

rtc_codecs_t* _default_codecs = nullptr;
static rtc_codec_t* _clone_codec(rtc_codec_t* s);
static rtc_codec_t* _new_codec_vp8(bool enable_bwe);
static rtc_codec_t* _new_codec_h264(bool enable_bwe);
static rtc_codec_t* _new_codec_red();
static rtc_codec_t* _new_codec_ulpfec();
static rtc_codec_t* _new_codec_pcmu();
static rtc_codec_t* _new_codec_pcma();
static rtc_codec_t* _new_codec_opus();
static bool         _caps_equal(rtc_codec_caps_t& caps_a, rtc_codec_caps_t& caps_b);
static bool         _support_codec(rtc_codec_t* c, rtc_codecs_t* candidate_codecs);
static bool         _is_h264_42e01f(rtc_codec_t* c);
static bool         _is_h264_baseline_profile(rtc_codec_t* c);
static void         _filter_support_rtcp_fb(rtc_codec_t* c);
static void         _reduce_rtx_and_unknown(rtc_codecs_t* codecs);
static void         _build_index(rtc_codecs_t* codecs);
/*
 * PUBLIC
 */

rtc_codec_t* rtc_codec_new() {
    auto c = new rtc_codec_t();
    c->id = rtc_codec_id_unknown;
    c->pt = 0;
    c->clock_rate = 0;
    c->channel_nb = 1;
    c->rtx_enabled = false;
    return c;
}

void rtc_codec_free(rtc_codec_t* codec) {
    delete codec;
}

std::ostream& operator<< (std::ostream& os, const rtc_codec_t& codec) {
    os << "name=" << rtc_codec_name(codec.id) << " ";
    os << "pt=" << static_cast<int>(codec.pt) << " ";
    os << "clockrate=" << codec.clock_rate << " ";
    os << "channel_nb=" << static_cast<int>(codec.channel_nb) << " ";
    os << "fb=[";
    for (auto fb : codec.feedbacks) os << fb << ",";
    os << "] caps=[";
    for (auto cap : codec.caps) os << cap << ",";
    os << "] rtx=";
    os << std::boolalpha << codec.rtx_enabled << " ";
    os << "rtx_pt=" << static_cast<int>(codec.rtx_pt);
    return os;
}

void rtc_codec_init(bool enable_fec, bool enable_bwe) {
    _default_codecs = rtc_codecs_new();
    rtc_codecs_add(_default_codecs, _new_codec_vp8(enable_bwe));
    rtc_codecs_add(_default_codecs, _new_codec_h264(enable_bwe));
    if (enable_fec) {
        rtc_codecs_add(_default_codecs, _new_codec_red());
        rtc_codecs_add(_default_codecs, _new_codec_ulpfec());
    }
    rtc_codecs_add(_default_codecs, _new_codec_pcmu());
    rtc_codecs_add(_default_codecs, _new_codec_pcma());
    rtc_codecs_add(_default_codecs, _new_codec_opus());
    _build_index(_default_codecs);
}

void rtc_codec_uninit() {
    rtc_codecs_free(_default_codecs);
}

rtc_codec_id_t rtc_codec_id(const char* name) {
    if (strcasecmp(name, "vp8") == 0) return rtc_codec_id_vp8;
    if (strcasecmp(name, "h264") == 0) return rtc_codec_id_h264;
    if (strcasecmp(name, "pcmu") == 0) return rtc_codec_id_pcmu;
    if (strcasecmp(name, "pcma") == 0) return rtc_codec_id_pcma;
    if (strcasecmp(name, "opus") == 0) return rtc_codec_id_opus;
    if (strcasecmp(name, "red") == 0) return rtc_codec_id_red;
    if (strcasecmp(name, "rtx") == 0) return rtc_codec_id_rtx;
    if (strcasecmp(name, "ulpfec") == 0) return rtc_codec_id_ulpfec;
    return rtc_codec_id_unknown;
}

const char* rtc_codec_name(rtc_codec_id_t id) {
    switch (id) {
        case rtc_codec_id_vp8:
            return "VP8";
        case rtc_codec_id_h264:
            return "H264";
        case rtc_codec_id_pcmu:
            return "PCMU";
        case rtc_codec_id_pcma:
            return "PCMA";
        case rtc_codec_id_opus:
            return "opus";
        case rtc_codec_id_red:
            return "red";
        case rtc_codec_id_rtx:
            return "rtx";
        case rtc_codec_id_ulpfec:
            return "ulpfec";
        default:
        break;
    }
    return "unknown";
}

rtc_codec_type_t  rtc_codec_type(rtc_codec_id_t id) {
    switch (id) {
        case rtc_codec_id_vp8:
            return rtc_codec_type_video;
        case rtc_codec_id_h264:
            return rtc_codec_type_video;
        case rtc_codec_id_pcmu:
            return rtc_codec_type_audio;
        case rtc_codec_id_pcma:
            return rtc_codec_type_audio;
        case rtc_codec_id_opus:
            return rtc_codec_type_audio;
        case rtc_codec_id_red:
            return rtc_codec_type_other;
        case rtc_codec_id_rtx:
            return rtc_codec_type_other;
        case rtc_codec_id_ulpfec:
            return rtc_codec_type_other;
        default:
        break;
    }
    return rtc_codec_type_other;
}

rtc_codecs_t* rtc_codecs_new() {
    auto codecs = new rtc_codecs_t();
    return codecs;
}

void rtc_codecs_free(rtc_codecs_t* codecs) {
    for (auto codec : codecs->codecs) {
        rtc_codec_free(codec);
    }
    codecs->codecs.clear();
    codecs->codecs_by_id.clear();
    codecs->codecs_by_pt.clear();
    codecs->codecs_by_rtx_pt.clear();
}

void rtc_codecs_add(rtc_codecs_t* codecs, rtc_codec_t* codec) {
    codecs->codecs.emplace_back(codec);
    codecs->codecs_by_pt.emplace(codec->pt, codec);
}

rtc_codec_t* rtc_codecs_find_by_pt(rtc_codecs_t *codecs, uint8_t pt) {
    auto it = codecs->codecs_by_pt.find(pt);
    if (it == codecs->codecs_by_pt.end()) return nullptr;
    return it->second;
}

rtc_codec_t* rtc_codecs_find_by_rtx_pt(rtc_codecs_t *codecs, uint8_t pt) {
    auto it = codecs->codecs_by_rtx_pt.find(pt);
    if (it == codecs->codecs_by_rtx_pt.end()) return nullptr;
    return it->second;
}

rtc_codec_t* rtc_codecs_find_by_id(rtc_codecs_t *codecs, rtc_codec_id_t id) {
    auto it = codecs->codecs_by_id.find(id);
    if (it == codecs->codecs_by_id.end()) return nullptr;
    return it->second;
}

void rtc_codecs_delete(rtc_codecs_t *codecs, uint8_t pt) {
    rtc_codec_t* codec = nullptr;
    auto it = codecs->codecs.begin();
    while (it != codecs->codecs.end()) {
        codec = *it;
        if (codec->pt == pt) {
            codecs->codecs_by_pt.erase(codec->pt);
            codecs->codecs.erase(it);
            rtc_codec_free(codec);
            break;
        }
        ++it;
    }
    return;
}

void rtc_codecs_dump(rtc_codecs_t* codecs) {
    for (auto codec : codecs->codecs) {
        std::ostringstream writer;
        writer << *codec;
        dbgi("\tcodec %s", writer.str().c_str());
    }
}

void _append_if_absent(std::list<std::string>& hints, const std::string& hint) {
    for (auto& h : hints) {
        if (h == hint) return;
    }
    hints.emplace_back(hint);
}

rtc_codecs_t* rtc_codecs_default(std::list<std::string>& hints) {
    auto codecs = rtc_codecs_new();

    _append_if_absent(hints, "VP8");
    _append_if_absent(hints, "H264");
    _append_if_absent(hints, "red");
    _append_if_absent(hints, "ulpfec");
    _append_if_absent(hints, "opus");
    _append_if_absent(hints, "PCMU");
    _append_if_absent(hints, "PCMA");

    for (auto codec_name : hints) {
        auto codec_id = rtc_codec_id(codec_name.c_str());
        auto codec = rtc_codecs_find_by_id(_default_codecs, codec_id);
        if (codec) {
            rtc_codecs_add(codecs, _clone_codec(codec));
        }
    }
    _build_index(codecs);
    return codecs;
}

void rtc_codecs_negotiate(rtc_codecs_t* remote_codecs) {
    _reduce_rtx_and_unknown(remote_codecs);
    std::list<rtc_codec_t*> codecs;
    std::copy(remote_codecs->codecs.begin(), remote_codecs->codecs.end(), std::back_inserter(codecs));

    rtc_codec_t* first_42e01f_h264 = nullptr;
    rtc_codec_t* first_base_profile_h264 = nullptr;

    rtc_codec_t* first_video_codec = nullptr;
    for (auto codec : codecs) {
        if (!_support_codec(codec, _default_codecs)) {
            rtc_codecs_delete(remote_codecs, codec->pt);
            continue;
        }

        if(!first_video_codec
            && rtc_codec_type(codec->id) == rtc_codec_type_video){
            first_video_codec = codec;
        }

        if(first_video_codec && first_video_codec->id == rtc_codec_id_vp8){
            continue;
        }

        if(!first_42e01f_h264 && _is_h264_42e01f(codec)){
            first_42e01f_h264 = codec;
        }

        if(!first_base_profile_h264 && _is_h264_baseline_profile(codec)) {
            first_base_profile_h264 = codec;
        }
    }

    //对于H264编码，优先选择 42e01f，base_profile，然后才是第一个H264
    rtc_codec_t* use_h264 = first_42e01f_h264 ? first_42e01f_h264 : first_base_profile_h264;

    // first_video_codec H264编码，应当 use_h264 与 first_video_codec 交换顺序
    // first_video_codec VP8时，保持不变
    if(first_video_codec
        && first_video_codec->id == rtc_codec_id_h264
        && use_h264
        && use_h264 != first_video_codec){
            std::list<rtc_codec_t*>::iterator first_video_codec_it;
            std::list<rtc_codec_t*>::iterator use_h264_it;
            for(std::list<rtc_codec_t*>::iterator it=remote_codecs->codecs.begin(); it!=remote_codecs->codecs.end(); it++){
                if(*it == use_h264){
                    use_h264_it = it;
                }
                if(*it == first_video_codec){
                    first_video_codec_it = it;
                }
            }
            std::swap(*first_video_codec_it, *use_h264_it);
    }

    for (auto codec : remote_codecs->codecs) {
        _filter_support_rtcp_fb(codec);
    }

    _build_index(remote_codecs);
    return;
}

bool rtc_codecs_video_has_transport_cc(rtc_codecs_t *codecs) {
    for (rtc_codec_t* codec : codecs->codecs) {
        if (codec->id == rtc_codec_id_vp8 || codec->id == rtc_codec_id_h264) {
            auto transport_cc_it = std::find(codec->feedbacks.begin(), codec->feedbacks.end(), "transport-cc");
            if (transport_cc_it != codec->feedbacks.end()) {
                return true;
            }
        }
    }
    return false;
}

uint8_t rtc_codec_default_pt(rtc_codec_id_t id, rtc_codec_id_t fec_id) {
    switch (id) {
    case rtc_codec_id_vp8:
        return rtc_codec_default_pt_vp8;
    case rtc_codec_id_h264:
        return rtc_codec_default_pt_h264;
    case rtc_codec_id_pcmu:
        return rtc_codec_default_pt_pcmu;
    case rtc_codec_id_pcma:
        return rtc_codec_default_pt_pcma;
    case rtc_codec_id_opus:
        return rtc_codec_default_pt_opus;
    case rtc_codec_id_red:
        return rtc_codec_default_pt_red;
    case rtc_codec_id_ulpfec:
        switch (fec_id) {
        case rtc_codec_id_vp8:
            return rtc_codec_default_pt_ulpfec_vp8;
        case rtc_codec_id_h264:
            return rtc_codec_default_pt_ulpfec_h264;
        case rtc_codec_id_unknown:
            return rtc_codec_default_pt_ulpfec_unknown;
        default:
            break;
        }
        break;
    default:
        break;
    }
    dbgi("could not get pt of codec '%s', fec codec '%s'", rtc_codec_name(id), rtc_codec_name(fec_id));
    return 255;
}

/*
 * PRIVATE FUNC IMPL
 */



rtc_codec_t *_clone_codec(rtc_codec_t *s) {
    auto d = new rtc_codec_t();
    d->id = s->id;
    d->pt = s->pt;
    d->clock_rate = s->clock_rate;
    d->channel_nb = s->channel_nb;
    std::copy(s->feedbacks.begin(), s->feedbacks.end(), std::back_inserter(d->feedbacks));
    std::copy(s->caps.begin(), s->caps.end(), std::back_inserter(d->caps));
    d->rtx_enabled = s->rtx_enabled;
    d->rtx_pt = s->rtx_pt;
    return d;
}

rtc_codec_t *_new_codec_vp8(bool enable_bwe) {
    rtc_codec_t* c = rtc_codec_new();
    c->id = rtc_codec_id_vp8;
    c->pt = 96;
    c->clock_rate = 90000;
    c->feedbacks.emplace_back("nack");
    c->feedbacks.emplace_back("nack pli");
    c->feedbacks.emplace_back("goog-remb");
    if (enable_bwe) {
        c->feedbacks.emplace_back("transport-cc");
    }
    c->rtx_enabled = true;
    c->rtx_pt = 97;
    return c;
}

static rtc_codec_t* _new_codec_h264(bool enable_bwe) {
    rtc_codec_t* c = rtc_codec_new();
    c->id = rtc_codec_id_h264;
    c->pt = 100;
    c->clock_rate = 90000;
    c->feedbacks.emplace_back("nack");
    c->feedbacks.emplace_back("nack pli");
    c->feedbacks.emplace_back("goog-remb");
    if (enable_bwe) {
        c->feedbacks.emplace_back("transport-cc");
    }
    c->caps.emplace_back("level-asymmetry-allowed=1");
    c->caps.emplace_back("packetization-mode=1");
    c->caps.emplace_back("profile-level-id=42e01f");
    c->rtx_enabled = true;
    c->rtx_pt = 101;
    return c;
}

static rtc_codec_t* _new_codec_red() {
    rtc_codec_t* c = rtc_codec_new();
    c->id = rtc_codec_id_red;
    c->pt = 102;
    c->clock_rate = 90000;
    c->rtx_enabled = true;
    c->rtx_pt = 125;
    return c;
}

static rtc_codec_t* _new_codec_ulpfec() {
    rtc_codec_t* c = rtc_codec_new();
    c->id = rtc_codec_id_ulpfec;
    c->pt = 127;
    c->clock_rate = 90000;
    return c;
}

static rtc_codec_t* _new_codec_pcmu() {
    rtc_codec_t* c = rtc_codec_new();
    c->id = rtc_codec_id_pcmu;
    c->pt = 0;
    c->clock_rate = 8000;
    return c;
}

static rtc_codec_t* _new_codec_pcma() {
    rtc_codec_t* c = rtc_codec_new();
    c->id = rtc_codec_id_pcma;
    c->pt = 8;
    c->clock_rate = 8000;
    return c;
}

static rtc_codec_t* _new_codec_opus() {
    rtc_codec_t* c = rtc_codec_new();
    c->id = rtc_codec_id_opus;
    c->pt = 111;
    c->clock_rate = 48000;
    c->channel_nb = 2;
    c->caps.emplace_back("minptime=10");
    c->caps.emplace_back("useinbandfec=1");
    return c;
}

static bool _caps_equal(rtc_codec_caps_t& caps_a, rtc_codec_caps_t& caps_b) {
    // mobile sdk gives H.264 without any params, we nevertheless accept that
    if (caps_a.empty() || caps_b.empty()) return true;
    if (caps_a == caps_b) return true;
    if (caps_a.empty() || caps_b.empty()) return false;
    if (caps_a.size() != caps_b.size()) return false;
    caps_a.sort();
    caps_b.sort();
    auto it_a = caps_a.begin();
    auto it_b = caps_b.begin();
    while (it_a != caps_a.end() && it_b != caps_b.end()) {
        if (*it_a != *it_b) return false;
        it_a++;
        it_b++;
    }
    return true;
}

static bool _caps_has(rtc_codec_caps_t& caps_a, rtc_codec_cap_t str) {
    // mobile sdk gives H.264 without any params, we nevertheless accept that
    if (caps_a.empty()) return true;
    auto it_a = caps_a.begin();
    while (it_a != caps_a.end()) {
        if (*it_a == str) return true;
        it_a++;
    }
    return false;
}

static bool _is_h264_42e01f(rtc_codec_t* c){
    bool flag = c->id == rtc_codec_id_h264
        && _caps_has(c->caps, "packetization-mode=1")
        && _caps_has(c->caps, "profile-level-id=42e01f")
        && _caps_has(c->caps, "level-asymmetry-allowed=1");
    return flag;
}

static rtc_codec_cap_t _get_rtc_codec_cap(rtc_codec_t* c, rtc_codec_cap_t prefix){
    if (c->caps.empty()) return "";
    auto it_a = c->caps.begin();
    while (it_a != c->caps.end()) {
        bool isPrefix = it_a->length() < prefix.length() ? false : it_a->substr(0, prefix.length()) == prefix;
        if(isPrefix) {
            return *it_a;
        }
        it_a++;
    }
    return "";
}

static bool _is_h264_baseline_profile(rtc_codec_t* c){
    if(c->id != rtc_codec_id_h264){
        return false;
    }

    if(!_caps_has(c->caps, "packetization-mode=1")){
        return false;
    }

    rtc_codec_cap_t prefix = "profile-level-id=";
    rtc_codec_cap_t profile = _get_rtc_codec_cap(c, prefix);
    if(profile == ""){
        return false;
    }

    if(profile.length() <= prefix.length()){
        return false;
    }

    std::string value = profile.substr(prefix.length());
    if(value.length() < 6){
        return false;
    }

    std::string profile_idc = value.substr(0, 2); //
    if(profile_idc == "42"){
        return true;
    }

    char iop = value[3]; // profile_iop 第一为1
    return iop >= '8';
}

static bool _support_codec(rtc_codec_t* c, rtc_codecs_t* candidate_codecs) {
    for (auto candidate_codec : candidate_codecs->codecs) {
        if (candidate_codec->id != c->id) continue;
        if (candidate_codec->clock_rate != c->clock_rate) continue;
        if (candidate_codec->channel_nb != c->channel_nb) continue;
        switch(c->id) {
            case rtc_codec_id_h264:
                //if (_caps_equal(candidate_codec->caps, c->caps)) return true;
                if (_caps_has(c->caps, "packetization-mode=1")) return true;
                continue;
            case rtc_codec_id_vp8:
            case rtc_codec_id_pcmu:
            case rtc_codec_id_pcma:
            case rtc_codec_id_opus:
            case rtc_codec_id_red:
            case rtc_codec_id_ulpfec:
                return true;
            default:
                break;
        }
    }
    return false;
}

static void _filter_support_rtcp_fb(rtc_codec_t* c) {
    rtc_codec_feedbacks_t fbs;
    auto default_it = _default_codecs->codecs_by_id.find(c->id);
    if (default_it == _default_codecs->codecs_by_id.end()) {
        return;
    }
    auto default_codec = default_it->second;
    for (auto fb : c->feedbacks) {
        auto cap_it = std::find(default_codec->feedbacks.begin(), default_codec->feedbacks.end(), fb);
        if (cap_it != default_codec->feedbacks.end()) {
            fbs.emplace_back(fb);
        }
//        if (fb == "nack" || fb == "nack pli" || fb == "goog-remb" || fb == "transport-cc") {
//
//        }
    }
    c->feedbacks = fbs;
}

static void _reduce_rtx_and_unknown(rtc_codecs_t *codecs) {
    auto it = codecs->codecs.begin();
    while (it != codecs->codecs.end()) {
        auto codec = *it;
        it++;
        switch (codec->id) {
            case rtc_codec_id_rtx:
                for (auto cap : codec->caps) {
                    if (cap.substr(0, 4) == "apt=") {
                        uint8_t apt = static_cast<uint8_t>(atoi(cap.substr(4).c_str()));
                        auto acodec = rtc_codecs_find_by_pt(codecs, apt);
                        if (acodec) {
                            acodec->rtx_enabled = true;
                            acodec->rtx_pt = codec->pt;
                        }
                    }
                }
                rtc_codecs_delete(codecs, codec->pt);
                break;
            case rtc_codec_id_unknown:
                rtc_codecs_delete(codecs, codec->pt);
                break;
            default:
                break;
        }
    }
}

static void _build_index(rtc_codecs_t* codecs) {
    codecs->codecs_by_id.clear();
    codecs->codecs_by_pt.clear();
    codecs->codecs_by_rtx_pt.clear();
    for (auto codec : codecs->codecs) {
        codecs->codecs_by_id.emplace(codec->id, codec);
        codecs->codecs_by_pt.emplace(codec->pt, codec);
        if (codec->rtx_enabled) {
            codecs->codecs_by_rtx_pt.emplace(codec->rtx_pt, codec);
        }
    }
}